﻿/*	Copyright (c) 2018 Jean-Marc VIGLINO,
  released under the CeCILL-B license (French BSD license)
  (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt).
*/
import ol_control_Control from 'ol/control/Control.js'
import ol_Feature from 'ol/Feature.js'
import ol_ext_element from '../util/element.js';
import ol_control_SearchGeoportail from './SearchGeoportail.js'
import ol_source_Vector from 'ol/source/Vector.js'
import ol_geom_Point from 'ol/geom/Point.js'
import {transform as ol_proj_transform} from 'ol/proj.js'
import { ol_coordinate_equal } from '../geom/GeomUtils.js'
import ol_format_GeoJSON from 'ol/format/GeoJSON.js'

/** Geoportail routing Control.
 * @constructor
 * @extends {ol_control_Control}
 * @fires select
 * @fires change:input
 * @fires routing:start
 * @fires routing
 * @fires step:select
 * @fires step:hover
 * @fires error
 * @fires abort
 * @param {Object=} options
 *	@param {string} options.className control class name
 *	@param {string} [options.leng=en] control language
 *	@param {string | undefined} [options.apiKey] the service api key.
 *	@param {string | undefined} options.authentication: basic authentication for the service API as btoa("login:pwd")
 *	@param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport.
 *	@param {string | undefined} options.label Text label to use for the search button, default "search"
 *	@param {string | undefined} options.placeholder placeholder, default "Search..."
 *	@param {string | undefined} options.inputLabel label for the input, default none
 *	@param {string | undefined} options.noCollapse prevent collapsing on input blur, default false
 *	@param {number | undefined} options.typing a delay on each typing to start searching (ms) use -1 to prevent autocompletion, default 300.
 *	@param {integer | undefined} options.minLength minimum length to start searching, default 1
 *	@param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10
 *	@param {integer | undefined} options.maxHistory maximum number of items to display in history. Set -1 if you don't want history, default maxItems
 *	@param {function} options.getTitle a function that takes a feature and return the name to display in the index.
 *	@param {function} options.autocomplete a function that take a search string and callback function to send an array
 *	@param {number} options.timeout default 20s
 */
var ol_control_RoutingGeoportail = class olcontrolRoutingGeoportail extends ol_control_Control {
  constructor(options) {
    options = options || {};
    if (options.typing == undefined) options.typing = 300;
    options.apiKey = options.apiKey || 'itineraire';
    if (!options.search) options.search = {};
    options.search.apiKey = options.search.apiKey || 'essentiels';
    
    var element = document.createElement("DIV");
    super({
      element: element,
      target: options.target
    });
    
    var self = this;
    // Class name for history
    this._classname = options.className || 'search';

    this._source = new ol_source_Vector();
    this.set('lang', options.lang || 'en')

    // Authentication
    this._auth = options.authentication;

    var classNames = (options.className || "") + " ol-routing";
    if (!options.target) {
      classNames += " ol-unselectable ol-control";
    }
    element.setAttribute('class', classNames);
    if (!options.target) {
      var bt = ol_ext_element.create('BUTTON', { parent: element });
      bt.addEventListener('click', function () {
        element.classList.toggle('ol-collapsed');
      });
    }

    this.set('url', 'https://data.geopf.fr/navigation/itineraire')

    var content = ol_ext_element.create('DIV', { className: 'content', parent: element });

    var listElt = ol_ext_element.create('DIV', { className: 'search-input', parent: content });

    this._search = [];
    this.addSearch(listElt, options);
    this.addSearch(listElt, options);

    ol_ext_element.create('I', { 
      className: 'ol-car', 
      title: options.carlabel || 'by car', 
      parent: content 
    })
      .addEventListener("click", function () {
        self.setMode('car');
      });
    ol_ext_element.create('I', { 
      className: 'ol-pedestrian', 
      title: options.pedlabel || 'pedestrian', 
      parent: content 
    })
      .addEventListener("click", function () {
        self.setMode('pedestrian');
      });
    ol_ext_element.create('I', { className: 'ol-ok', title: options.runlabel || 'search', html: 'OK', parent: content })
      .addEventListener("click", function () {
        self.calculate();
      });
    ol_ext_element.create('I', { className: 'ol-cancel', html: 'cancel', parent: content })
      .addEventListener("click", function () {
        this.resultElement.innerHTML = '';
      }.bind(this));

    this.resultElement = document.createElement("DIV");
    this.resultElement.setAttribute('class', 'ol-result');
    element.appendChild(this.resultElement);

    this.setMode(options.mode || 'car');
    this.set('timeout', options.timeout || 20000);
  }
  setMode(mode, silent) {
    this.set('mode', mode);
    this.element.querySelector(".ol-car").classList.remove("selected");
    this.element.querySelector(".ol-pedestrian").classList.remove("selected");
    this.element.querySelector(".ol-" + mode).classList.add("selected");
    if (!silent)
      this.calculate();
  }
  setMethod(method, silent) {
    this.set('method', method);
    if (!silent)
      this.calculate();
  }
  addButton(className, title, info) {
    var bt = document.createElement("I");
    bt.setAttribute("class", className);
    bt.setAttribute("type", "button");
    bt.setAttribute("title", title);
    bt.innerHTML = info || '';
    this.element.appendChild(bt);
    return bt;
  }
  /** Get point source
   * @return {ol.source.Vector }
   */
  getSource() {
    return this._source;
  }
  _resetArray(element) {
    this._search = [];
    var q = element.parentNode.querySelectorAll('.search-input > div');
    q.forEach(function (d) {
      if (d.olsearch) {
        if (d.olsearch.get('feature')) {
          d.olsearch.get('feature').set('step', this._search.length);
          if (this._search.length === 0)
            d.olsearch.get('feature').set('pos', 'start');
          else if (this._search.length === q.length - 1)
            d.olsearch.get('feature').set('pos', 'end');
          else
            d.olsearch.get('feature').set('pos', '');
        }
        this._search.push(d.olsearch);
      }
    }.bind(this));
  }
  /** Remove a new search input
   * @private
   */
  removeSearch(element, options, after) {
    element.removeChild(after);
    if (after.olsearch.get('feature'))
      this._source.removeFeature(after.olsearch.get('feature'));
    if (this.getMap())
      this.getMap().removeControl(after.olsearch);
    this._resetArray(element);
  }
  /** Add a new search input
   * @private
   */
  addSearch(element, options, after) {
    var self = this;
    var div = ol_ext_element.create('DIV');
    if (after)
      element.insertBefore(div, after.nextSibling);
    else
      element.appendChild(div);

    ol_ext_element.create('BUTTON', { title: options.startlabel || 'use shift to add / ctrl to remove', parent: div })
      .addEventListener('click', function (e) {
        if (e.ctrlKey) {
          if (this._search.length > 2)
            this.removeSearch(element, options, div);
        } else if (e.shiftKey) {
          this.addSearch(element, options, div);
        }
      }.bind(this));

    var search = div.olsearch = new ol_control_SearchGeoportail({
      className: 'IGNF ol-collapsed',
      apiKey: options.search.apiKey,
      authentication: options.search.authentication,
      target: div,
      reverse: true
    });
    search._changeCounter = 0;
    this._resetArray(element);
    search.on('select', function (e) {
      search.setInput(e.search.fulltext);
      var f = search.get('feature');
      if (!f) {
        f = new ol_Feature(new ol_geom_Point(e.coordinate));
        search.set('feature', f);
        this._source.addFeature(f);
        // Check geometry change
        search.checkgeom = true;
        f.getGeometry().on('change', function () {
          if (search.checkgeom)
            this.onGeometryChange(search, f);
        }.bind(this));
      } else {
        search.checkgeom = false;
        if (!e.silent)
          f.getGeometry().setCoordinates(e.coordinate);
        search.checkgeom = true;
      }
      f.set('name', search.getTitle(e.search));
      f.set('step', this._search.indexOf(search));
      if (f.get('step') === 0)
        f.set('pos', 'start');
      else if (f.get('step') === this._search.length - 1)
        f.set('pos', 'end');
      search.set('selection', e.search);
    }.bind(this));
    search.element.querySelector('input').addEventListener('change', function () {
      search.set('selection', null);
      self.resultElement.innerHTML = '';
    });
    if (this.getMap())
      this.getMap().addControl(search);
  }
  /** Geometry has changed
   * @private
   */
  onGeometryChange(search, f, delay) {
    // Set current geom 
    var lonlat = ol_proj_transform(f.getGeometry().getCoordinates(), this.getMap().getView().getProjection(), 'EPSG:4326');
    search._handleSelect({
      x: lonlat[0],
      y: lonlat[1],
      fulltext: lonlat[0].toFixed(6) + ',' + lonlat[1].toFixed(6)
    }, true, { silent: true });

    // Try to revers geocode
    if (delay) {
      search._changeCounter--;
      if (!search._changeCounter) {
        search.reverseGeocode(f.getGeometry().getCoordinates(), { silent: true });
        return;
      }
    } else {
      search._changeCounter++;
      setTimeout(function () {
        this.onGeometryChange(search, f, true);
      }.bind(this), 1000);
    }
  }
  /**
   * Set the map instance the control is associated with
   * and add its controls associated to this map.
   * @param {_ol_Map_} map The map instance.
   */
  setMap(map) {
    super.setMap(map);

    for (var i = 0; i < this._search.length; i++) {
      var c = this._search[i];
      c.setMap(map);
    }
  }
  /** Get request data
   * @private
   */
  requestData(steps) {
    var start = steps[0];
    var end = steps[steps.length - 1];
    var waypoints = '';
    for (var i = 1; i < steps.length - 1; i++) {
      waypoints += (waypoints ? '|' : '') + steps[i].x + ',' + steps[i].y;
    }
    return {
      resource: 'bdtopo-osrm',
      profile: this.get('mode') === 'pedestrian' ? 'pedestrian' : 'car',
      optimization: this.get('mode') === 'pedestrian' ? '' : this.get('method') || 'fastest',
      start: start.x + ',' + start.y,
      end: end.x + ',' + end.y,
      intermediates: waypoints,
      geometryFormat: 'geojson'
    };
  }
  /** Gets time as string
   * @param {*} routing routing response
   * @return {string}
   * @api
   */
  getTimeString(t) {
    t /= 60;
    return (t < 1) ? '' : (t < 60) ? t.toFixed(0) + ' min' : (t / 60).toFixed(0) + ' h ' + (t % 60).toFixed(0) + ' min';
  }
  /** Gets distance as string
   * @param {number} d distance
   * @return {string}
   * @api
   */
  getDistanceString(d) {
    return (d < 1000) ? d.toFixed(0) + ' m' : (d / 1000).toFixed(2) + ' km';
  }
  /** Show routing as a list
   * @private
   */
  listRouting(routing) {
    this.resultElement.innerHTML = '';
    var t = this.getTimeString(routing.duration);
    t += ' (' + this.getDistanceString(routing.distance) + ')';
    var iElement = document.createElement('i');
    iElement.textContent = t;
    this.resultElement.appendChild(iElement);

    var ul = document.createElement('ul');
    this.resultElement.appendChild(ul);

    var infoType = ol_control_RoutingGeoportail.prototype.instructions[this.get('lang') || 'en']
    var infoClassName = {
      'straight': '',
      'left': 'L',
      'right': 'R',
      'slight left': 'FL',
      'slight right': 'FR',
    }

    routing.features.forEach(function (f, i) {
      var d = this.getDistanceString(f.get('distance'));
      t = this.getTimeString(f.get('durationT'));
      // Decode instructions
      var instruction = infoType[f.get('instruction_type')] || infoType['none'];
      instruction += ' ' + (infoType[f.get('instruction_modifier')] || infoType.straight) + ' ';
      // console.log(f.get('instruction_type'), '-',f.get('instruction_modifier'))
      // Show info
      ol_ext_element.create('LI', {
        className: infoClassName[f.get('instruction_modifier')] || '',
        html: (instruction || '#')
          + ' ' + f.get('name')
          + '<i>' + d + (t ? ' - ' + t : '') + '</i>',
        on: {
          pointerenter: function () {
            this.dispatchEvent({ type: 'step:hover', hover: false, index: i, feature: f });
          }.bind(this),
          pointerleave: function () {
            this.dispatchEvent({ type: 'step:hover', hover: false, index: i, feature: f });
          }.bind(this)
        },
        click: function () {
          this.dispatchEvent({ type: 'step:select', index: i, feature: f });
        }.bind(this),
        parent: ul
      });
    }.bind(this));
  }
  /** Handle routing response
   * @private
   */
  handleResponse(data, start, end) {
    if (data.status === 'ERROR') {
      this.dispatchEvent({
        type: 'errror',
        status: '200',
        statusText: data.message
      });
      return;
    }
    // console.log(data)
    var routing = { type: 'routing' };
    routing.features = [];
    var distance = 0;
    var duration = 0;
    var f;
    var parser = new ol_format_GeoJSON();
    var lastPt;
    for (var i = 0, l; l = data.portions[i]; i++) {
      for (var j = 0, s; s = l.steps[j]; j++) {
        s.type = 'Feature';
        s.properties = s.attributes.name || s.attributes;
        s.properties.distance = s.distance;
        s.properties.duration = Math.round(s.duration * 60);
        // Route info
        if (s.instruction) {
          s.properties.instruction_type = s.instruction.type;
          s.properties.instruction_modifier = s.instruction.modifier;
        }
        // Distance / time
        distance += s.distance;
        duration += s.duration;
        s.properties.distanceT = Math.round(distance * 100) / 100;
        s.properties.durationT = Math.round(duration * 60);
        s.properties.name = s.properties.cpx_toponyme_route_nommee || s.properties.cpx_toponyme || s.properties.cpx_numero || s.properties.nom_1_droite || s.properties.nom_1_gauche || '';
        // TODO: BUG ?
        var lp = s.geometry.coordinates[s.geometry.coordinates.length - 1];
        if (lastPt && !ol_coordinate_equal(lp, s.geometry.coordinates[s.geometry.coordinates.length - 1])) {
          s.geometry.coordinates.unshift(lastPt);
        }
        lastPt = s.geometry.coordinates[s.geometry.coordinates.length - 1];
        //
        f = parser.readFeature(s, {
          featureProjection: this.getMap().getView().getProjection()
        });
        routing.features.push(f);
      }
    }
    routing.distance = parseFloat(data.distance);
    routing.duration = parseFloat(data.duration) / 60;

    // Full route
    var route = parser.readGeometry(data.geometry, {
      featureProjection: this.getMap().getView().getProjection()
    });
    routing.feature = new ol_Feature({
      geometry: route,
      start: this._search[0].getTitle(start),
      end: this._search[0].getTitle(end),
      distance: routing.distance,
      duration: routing.duration
    });

    // console.log(data, routing);
    this.dispatchEvent(routing);
    this.path = routing;
    return routing;
  }
  /** Abort request
   */
  abort() {
    // Abort previous request
    if (this._request) {
      this._request.abort();
      this._request = null;
      this.dispatchEvent({ type: 'abort' });
    }
  }
  /** Calculate route
   * @param {Array<ol.coordinate>|undefined} steps an array of steps in EPSG:4326, default use control input values
   * @return {boolean} true is a new request is send (more than 2 points to calculate)
   */
  calculate(steps) {
    this.resultElement.innerHTML = '';
    if (steps) {
      var convert = [];
      steps.forEach(function (s) {
        convert.push({ x: s[0], y: s[1] });
      });
      steps = convert;
    } else {
      steps = [];
      for (var i = 0; i < this._search.length; i++) {
        if (this._search[i].get('selection'))
          steps.push(this._search[i].get('selection'));
      }
    }
    if (steps.length < 2)
      return false;

    var start = steps[0];
    var end = steps[steps.length - 1];

    var data = this.requestData(steps);

    var url = encodeURI(this.get('url'));

    var parameters = '';
    for (var index in data) {
      parameters += (parameters) ? '&' : '?';
      if (data.hasOwnProperty(index))
        parameters += index + '=' + data[index];
    }

    var self = this;
    this.dispatchEvent({ type: 'routing:start' });
    this.ajax(url + parameters,
      function (resp) {
        if (resp.status >= 200 && resp.status < 400) {
          self.listRouting(self.handleResponse(JSON.parse(resp.response), start, end));
        } else {
          //console.log(url + parameters, arguments);
          this.dispatchEvent({ type: 'error', status: resp.status, statusText: resp.statusText });
        }
      }.bind(this),
      function (resp) {
        // console.log('ERROR', resp)
        this.dispatchEvent({ type: 'error', status: resp.status, statusText: resp.statusText });
      }.bind(this)
    );

    return true;
  }
  /** Send an ajax request (GET)
   * @param {string} url
   * @param {function} onsuccess callback
   * @param {function} onerror callback
   */
  ajax(url, onsuccess, onerror) {
    var self = this;

    // Abort previous request
    if (this._request) {
      this._request.abort();
    }

    // New request
    var ajax = this._request = new XMLHttpRequest();
    ajax.open('GET', url, true);
    ajax.timeout = this.get('timeout') || 20000;

    if (this._auth) {
      ajax.setRequestHeader("Authorization", "Basic " + this._auth);
    }
    this.element.classList.add('ol-searching');

    // Load complete
    ajax.onload = function () {
      self._request = null;
      self.element.classList.remove('ol-searching');
      onsuccess.call(self, this);
    };

    // Timeout
    ajax.ontimeout = function () {
      self._request = null;
      self.element.classList.remove('ol-searching');
      if (onerror)
        onerror.call(self, this);
    };

    // Oops, TODO do something ?
    ajax.onerror = function () {
      self._request = null;
      self.element.classList.remove('ol-searching');
      if (onerror)
        onerror.call(self, this);
    };

    // GO!
    ajax.send();
  }
}

/** Instructions labels  */
ol_control_RoutingGeoportail.prototype.instructions = {
  'en': {
    // Instruction type
    'none': 'Go ',
    'continue': 'Continue ',
    'new name': 'Continue ',
    'depart': 'Start',
    'arrive': 'Arrival',
    'turn': 'Turn',
    'fork': 'Fork',
    // Instruction modifier
    'straight': 'on',
    'left': 'left on',
    'right': 'right on',
    'slight left': 'slight left on',
    'slight right': 'slight right on',
  },
  'fr': {
    // Instruction type
    'none': 'Continuer ',
    'continue': 'Continuer ',
    'new name': 'Continuer ',
    'depart': 'Départ',
    'arrive': 'Arrivée',
    'turn': 'Tourner',
    'fork': 'Prendre',
    // Instruction modifier
    'straight': 'sur',
    'left': 'à gauche sur',
    'right': 'à droite sur',
    'slight left': 'légèrement à gauche sur',
    'slight right': 'légèrement à droite sur',
  }
};

export default ol_control_RoutingGeoportail
